/*  -*- mode: c++; coding: utf-8; c-file-style: "stroustrup"; -*-

    Copyright 2008 Asier Aguirre <asier.aguirre@gmail.com>
    This file is part of Intromit.

    Intromit is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Intromit is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Intromit.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "event_mng.hh"
#include "render_obj.hh"
#include "ogl.hh"
#include "key_names.hh"

#include <string.h>
#include <X11/XKBlib.h>
#include <sys/time.h>
#include <math.h>

// DEFINITIONS
#define CONFIGURE_MARGIN         4         // required for PWM
#define CONFIGURE_REDRAW                   // always redraw on configure
#define USEC_EVENTS           2500         // sample interval for events (in usecs)
#define INC_ACTION              32
#define MAX_KEY_NAME            15

#define CALLBACK_NOT_REFCNT

#define EM_CINEMATIC_K         1.0
#define EM_STOP_VECTOR_MOVE    1.0

CLASS_INIT(event_mng); // EVENT_MNG CLASS

event_mng::event_mng()
{
    CLASS_CTOR(event_mng);
    ctor();
}

event_mng::~event_mng()
{
    CLASS_DTOR(event_mng);
    dtor();
}

void event_mng::ctor()
{
    w=NULL;
    need_complete_redraw=true;
    need_update_from_back=false;
    memset(update_coor, 0, IS_WXYWH*sizeof(int));

    // callback
    callback=NULL;
    callback_obj=NULL;
    callback_data=NULL;
    callback_events=OGL_NOEVENTS_EVENT;

    // general status
    event_last=0;
    event_last_time=-1;

    // cursor
    cursor[IS_WX]=cursor[IS_WY]=-1;
    cursor_moved=false;
    cursor_in=true;

    // keyboard
    key_last_code=-1;
    key_last_status=0;
    key_last_press=false;
    for(int i=0; i<EM_KEYMAP_SIZE; i++)
    {
	keymap_status[i]=false;
	keymap_last_time[i]=0;
	keymap_accu_time[i]=0;
    }

    // actions
    util::ctor_vector(action, n_action, max_action);
    action_last=EM_ACTION_NONE;
    action_last_selector=IS_SEL_NONE;

    // action vector
    for(int i=0; i<EM_ACTION_N_VECTOR; i++)
    {
	action_vector_pos[i]=0;
	action_vector_posf[i]=0.0;
	action_vector_press[i][0]=action_vector_press[i][1]=false;
    }
    action_vector_time=time_now();
}

void event_mng::dtor()
{
    if(w) w=(ogl_window*)w->decref();

    // callback
    callback=NULL;
    if(callback_obj)
    {
#ifndef CALLBACK_NOT_REFCNT
	callback_obj->decref();
#endif
	callback_obj=NULL;
    }
    callback_data=NULL;

    // actions
    util::dtor_vector_delete(action, n_action, max_action);
}

bool event_mng::init(ogl_window *win)
{
    if(win) win->incref();
    if(w) w->decref();
    w=win;

    // register selector actions
    action_register_selector();

    return true;
}

int *event_mng::update_required()
{
    if(need_update_from_back)
    {
	need_update_from_back=false;
	return update_coor;
    }
    return NULL;
}

bool event_mng::redraw_required()
{
    bool dev=need_complete_redraw;
    need_complete_redraw=false;
    return dev;
}

int event_mng::event_dispatcher()
{
    int dev=USEC_EVENTS;
    XEvent e;
    while(w->get_event(&e, OGL_ALLX_EVENT))
    {
	dev=0;
	switch(e.type)
	{
	case MotionNotify:
	    event_motion(e.xmotion.x, e.xmotion.y, e.xmotion.time);
	    break;
	case KeyPress:
	case KeyRelease:
	    event_key(e.xkey.x, e.xkey.y, e.xkey.time, e.xkey.keycode, e.xkey.state, e.type==KeyPress);
	    break;
	case ButtonPress:
	case ButtonRelease:
	    event_button(e.xbutton.x, e.xbutton.y, e.xbutton.time, e.xbutton.button, e.xbutton.state, e.type==ButtonPress);
	    break;
	case Expose:
	    event_expose(e.xexpose.x, e.xexpose.y, e.xexpose.width, e.xexpose.height);
	    break;
	case ConfigureNotify:
	    event_configure(e.xconfigure.x, e.xconfigure.y, e.xconfigure.width, e.xconfigure.height, e.xconfigure.send_event);
	    break;
	case EnterNotify:
	case LeaveNotify:
	    event_enter_leave(e.type==EnterNotify);
	    break;
	case DestroyNotify:
	case UnmapNotify:
	default:
	    break;
	}
    }
    return dev;
}

void event_mng::event_configure(int x, int y, int width, int height, bool resize)
{
#ifndef CONFIGURE_REDRAW
    if(!need_complete_redraw)
    {
	if(resize || x<CONFIGURE_MARGIN || y<CONFIGURE_MARGIN) need_complete_redraw=true;
	else
	{
	    int root_width, root_height;
	    w->get_root_size(&root_width, &root_height);
	    if(x+width+CONFIGURE_MARGIN>root_width || y+height+CONFIGURE_MARGIN>root_height)
		need_complete_redraw=true;
	}
	if(need_complete_redraw) need_update_from_back=false;
    }
#else
    need_complete_redraw=true;
    need_update_from_back=false;
#endif
}

void event_mng::event_expose(int x, int y, int width, int height)
{
    if(!need_complete_redraw)
    {
	// convert to OpenGL coordinates
	y=w->get_height()-y-height;

	if(need_update_from_back)
	{
	    // extend zone
	    if(x<update_coor[IS_WX]) {update_coor[IS_WW]+=update_coor[IS_WX]-x; update_coor[IS_WX]=x;}
	    if(y<update_coor[IS_WY]) {update_coor[IS_WH]+=update_coor[IS_WY]-y; update_coor[IS_WY]=y;}
	    width+=x-update_coor[IS_WX];
	    height+=y-update_coor[IS_WY];
	    if(width>update_coor[IS_WW]) update_coor[IS_WW]=width;
	    if(height>update_coor[IS_WH]) update_coor[IS_WH]=height;
	}
	else
	{
	    // init zone
	    need_update_from_back=true;
	    update_coor[IS_WX]=x;
	    update_coor[IS_WY]=y;
	    update_coor[IS_WW]=width;
	    update_coor[IS_WH]=height;
	}
	//ERROR("UP:(%4d %4d)-(%4d %4d)", update_coor[IS_WX], update_coor[IS_WY], update_coor[IS_WW], update_coor[IS_WH]);
    }
}

void event_mng::event_motion(int x, int y, int time)
{
    event_last=OGL_MOUSE_MOVE_EVENT;
    event_last_time=time;
    cursor[IS_WX]=x;
    cursor[IS_WY]=w->get_height()-y-1;
    cursor_moved=true;
    if(callback_events & OGL_MOUSE_MOVE_EVENT) callback(callback_obj, callback_data);
}

void event_mng::event_button(int x, int y, int time, int button, int state, bool press)
{
    event_last=OGL_KEYBOARD_EVENT;
    event_last_time=time;
    key_last_code=EM_BUTKEY_OFF | button;
    key_last_status=state;
    key_last_press=press;
    event_update_keymap();
    if(callback_events & OGL_MOUSE_BUTTON_EVENT) callback(callback_obj, callback_data);
}

void event_mng::event_key(int x, int y, int time, int keycode, int state, bool press)
{
    event_last=OGL_KEYBOARD_EVENT;
    event_last_time=time;
    key_last_code=keycode;
    key_last_status=state;
    key_last_press=press;
    event_update_keymap();
    if(callback_events & OGL_KEYBOARD_EVENT) callback(callback_obj, callback_data);
}

void event_mng::event_enter_leave(bool enter)
{
    event_last=OGL_ENTER_LEAVE_EVENT;
    cursor_in=enter;
    if(!enter)
    {
	// set cursor out
	cursor_moved=true;
	cursor[IS_WX]=cursor[IS_WY]=-1;
    }
    if(callback_events & OGL_ENTER_LEAVE_EVENT) callback(callback_obj, callback_data);
}

void event_mng::event_update_keymap()
{
    // add time to accum
    if(keymap_status[key_last_code] && !key_last_press)
	keymap_accu_time[key_last_code]+=event_last_time-keymap_last_time[key_last_code];

    keymap_status[key_last_code]=key_last_press;
    keymap_last_time[key_last_code]=event_last_time;
}

void event_mng::set_callback(event_callback_type callback_, reference_counter *obj, void *data)
{
    callback=callback_;

#ifndef CALLBACK_NOT_REFCNT
    if(obj) obj->incref();
    if(callback_obj) callback_obj->decref();
#endif
    callback_obj=obj;

    callback_data=data;

    if(!callback) callback_events=OGL_NOEVENTS_EVENT;
}

void event_mng::set_callback_events(unsigned events)
{
    callback_events=events;
}

int event_mng::action_search(unsigned obj_id, unsigned obj_status, unsigned selector_id, unsigned action_spec)
{
    for(int i=0; i<n_action; i++)
    {
	if(!action[i]) continue;

	if(obj_id==action[i]->obj_id &&
	   obj_status==action[i]->obj_status &&
	   selector_id==action[i]->selector_id &&
	   action_spec==action[i]->action)
	    return i;
    }
    return -1;
}

int event_mng::action_search(unsigned obj_id, unsigned obj_status, unsigned selector_id, unsigned short key_code, unsigned short key_status)
{
    key_status&=0xff; // this mask is to use only keyboard modifiers, see X.h

    // dicotomic search by key_code and key_status
    int ini=0, end=n_action;
    while(end>ini)
    {
	int mid=(ini+end)>>1;
	if(action[mid]->key_code>key_code ||
	   (action[mid]->key_code==key_code && action[mid]->key_status>=key_status)) end=mid; else ini=mid+1;
    }
    while(ini<n_action && action[ini]->key_code==key_code && action[ini]->key_status==key_status)
    {
	if((action[ini]->obj_id==IS_TYPE_ANY || obj_id==action[ini]->obj_id) &&
	   (action[ini]->obj_status==IS_STATUS_ANY || IS_STATUS_SAME(obj_status, action[ini]->obj_status)) &&
	   selector_id==action[ini]->selector_id)
	    return ini;
	ini++;
    }
    return -1;
}

int event_mng::action_possibilities(int max_entries, int *entries, unsigned obj_id, unsigned selector_id, unsigned act)
{
    int n=0;
    for(int i=0; i<n_action && n<max_entries; i++)
    {
	if((action[i]->obj_id==IS_TYPE_ANY || obj_id==action[i]->obj_id) &&
	   ((!act && !(action[i]->obj_status&IS_STATUS_FOCUS)) ||
	    (act && action[i]->obj_status==(IS_STATUS_FOCUS | EM_GET_ACTION_TYPE(act)<<IS_STATUS_FOCUS_ROT))) &&
	   selector_id==action[i]->selector_id)
	    entries[n++]=i;
    }
    return n;
}

void event_mng::action_info(int i, unsigned short *key_code, unsigned short *key_status, const char **tip)
{
    if(i<0 || i>=n_action || !action[i])
    {
	*key_code=*key_status=0;
	*tip=NULL;
    }
    else
    {
	*key_code=action[i]->key_code;
	*key_status=action[i]->key_status;
	*tip=action[i]->tip;
    }
}

int event_mng::action_register(unsigned action_spec,
			       unsigned short default_keycode,
			       unsigned short default_keystatus,
			       unsigned obj_id,
			       unsigned obj_status,
			       unsigned selector_id,
			       const char *tip)
{
    int i;
    if((i=action_search(obj_id, obj_status, selector_id, action_spec))==-1)
    {
	// new one
	i=util::inc_vector(action, n_action, max_action, INC_ACTION);
	action[i]=new action_str;
	action[i]->key_code=default_keycode;
	action[i]->key_status=default_keystatus;
	action[i]->action=action_spec;
    }

    action[i]->obj_id=obj_id;
    action[i]->obj_status=obj_status;
    action[i]->selector_id=selector_id;
    action[i]->tip=tip;
    action_sort();
    return i;
}

unsigned event_mng::action_search(render_obj *obj, unsigned selector_id)
{
    int i;
    if(event_last==OGL_KEYBOARD_EVENT &&
       (i=action_search(obj->get_type(), obj->get_status(), selector_id, key_last_code, key_last_status))!=-1)
	return action[i]->action;
    return EM_ACTION_NONE;
}

void event_mng::action_dump()
{
    printf("%-4s %-8s %-8s %-8s %-4s %-4s %-15s %-8s %-2s %-2s %-2s %-2s %-2s\n",
	   "Num", "Obj id", "Obj stat", "Selector", "KeyC", "KeyS", "KeyString", "Action", "Ty", "Sq", "XY", "Cs", "Ev");
    for(int i=0; i<n_action; i++)
    {
	char buffer[MAX_KEY_NAME];
	key_to_string(action[i]->key_code, action[i]->key_status, buffer, MAX_KEY_NAME);

	printf("%4d %08x %08x %08x %04x %04x %-15s %08x %02x %02x %02x %02x %02x %s\n", i,
	       action[i]->obj_id,
	       action[i]->obj_status,
	       action[i]->selector_id,
	       action[i]->key_code,
	       action[i]->key_status,
	       buffer,
	       action[i]->action,
	       EM_GET_ACTION_TYPE(action[i]->action),
	       EM_GET_ACTION_SEQUENCE(action[i]->action),
	       EM_GET_ACTION_COORD(action[i]->action),
	       EM_GET_ACTION_CURSOR(action[i]->action),
	       EM_GET_ACTION_EVENT(action[i]->action),
	       action[i]->tip);
    }
}

void event_mng::action_unregister_all()
{
    util::dtor_vector_delete(action, n_action, max_action);    
}

int event_mng::action_sort_compare(const void *aa, const void *bb)
{
    action_str *a=*(action_str**)aa;
    action_str *b=*(action_str**)bb;
    if(!a) return 1;
    if(!b) return -1;
    if(a->key_code==b->key_code)
    {
	if(a->key_status==b->key_status)
	{
	    return a->obj_id<b->obj_id?-1:1;
	}
	else return a->key_status<b->key_status?-1:1;
    }
    else return a->key_code<b->key_code?-1:1;
}

void event_mng::action_sort()
{
    qsort(action, n_action, sizeof(action_str*), action_sort_compare);
    while(n_action>0 && !action[n_action-1]) n_action--;
}

bool event_mng::key_to_string(unsigned short key_code, unsigned short key_status, char *buffer, int n_buffer)
{
    *buffer=0;

    // parse some modifiers
    key_status&=0x20ff; // only keyboard
    if(key_status & ControlMask)
    {
	// control
	int n=snprintf(buffer, n_buffer, "C-");
	buffer+=n;
	n_buffer-=n;
	key_status&=~ControlMask;
    }
    if(key_status & Mod1Mask)
    {
	// meta
	int n=snprintf(buffer, n_buffer, "M-");
	buffer+=n;
	n_buffer-=n;
	key_status&=~Mod1Mask;
    }

    // mouse button
    if(key_code>EM_BUTKEY_OFF)
    {
	if(key_status & ShiftMask)
	{
	    // shift
	    int n=snprintf(buffer, n_buffer, "S-");
	    buffer+=n;
	    n_buffer-=n;
	    key_status&=~ShiftMask;
	}

	const char *but[3]={"B-L", "B-M", "B-R"};
	key_code-=EM_BUTKEY_OFF+1;
	if(key_code<3) strncpy(buffer, but[key_code], n_buffer);
	else snprintf(buffer, n_buffer, "B-%d", key_code);
	return true;
    }

    // ascii generators
    Display *dpy;
    if(w && (dpy=w->get_dpy()))
    {
	XKeyEvent event;
	event.display=dpy;
	event.type=KeyPress;
	event.keycode=key_code;
	event.state=key_status;
	int n=XLookupString(&event, buffer, n_buffer, NULL, NULL);
	if(n && (*buffer>32 || *buffer<0)) return true;
    }

    // more modifiers
    if(key_status & ShiftMask)
    {
	// shift
	int n=snprintf(buffer, n_buffer, "S-");
	buffer+=n;
	n_buffer-=n;
	key_status&=~ShiftMask;
    }

    // key names
    strncpy(buffer, key_names[key_code&0xff], n_buffer);
    return true;
}

bool event_mng::cursor_block(int coord[IS_2D])
{
    int wm=w->get_width()>>1;
    int hm=w->get_height()>>1;
    if(coord)
    {
	coord[IS_X]=cursor[IS_X]-wm;
	coord[IS_Y]=hm+1-w->get_height()+cursor[IS_Y];
    }
    if(!coord || coord[IS_X] || coord[IS_Y])
    {
	if(!XWarpPointer(w->get_dpy(), None, w->get_window(), 0, 0, 0, 0, wm, hm))
	    ERROR("XWarpPointer");
	cursor[IS_X]=wm;
	cursor[IS_Y]=w->get_height()-hm-1;
	return true;
    }
    return false;
}

void event_mng::cursor_set_position(int coord[IS_2D])
{
    if(!XWarpPointer(w->get_dpy(), None, w->get_window(), 0, 0, 0, 0, coord[IS_X], w->get_height()-coord[IS_Y]-1))
	ERROR("XWarpPointer");
    cursor[IS_X]=coord[IS_X];
    cursor[IS_Y]=coord[IS_Y];
}

int event_mng::time_now()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec*IS_USEC_SEC+tv.tv_usec;
}

unsigned event_mng::action_internal_dispatch(unsigned action)
{
    int type=EM_GET_ACTION_TYPE(action);
    if(type<EM_ACTION_TYPE_INT_VLAST)
    {
	// vectors
	int i=type-EM_ACTION_TYPE_INT_V0P;
	int j=i&1;   // 0: positive   1: negative
	i>>=1;       // vector #

	action_vector_update();
	action_vector_press[i][j]=get_key_last_press();	
	action=EM_SET_ACTION_EVENT(action, EM_ACTION_EVENT_VECTOR);
    }
    else
    {
	ERROR("TODO");
    }
    return action;
}

void event_mng::action_vector_update()
{
    // time from last update
    int now=time_now();
    int time_diff=now-action_vector_time;
    action_vector_time=now;

    // update current angles
    for(int i=0; i<EM_ACTION_N_VECTOR; i++)
    {
	if(action_vector_press[i][0] && !action_vector_press[i][1])
	    action_vector_pos[i]+=time_diff;
	else if(action_vector_press[i][1] && !action_vector_press[i][0])
	    action_vector_pos[i]-=time_diff;
    }
}

void event_mng::action_vector_updatef(float incs[EM_ACTION_N_VECTOR], float att)
{
    // time from last update
    int now=time_now();
    float time_diff=(float)(now-action_vector_time)*1e-3;
    action_vector_time=now;

    // attenuation in time
    att=powf(att, time_diff);

    // cinematics:
    // dv = vo + k dt
    // dx = vo dt + k dt^2 / 2
    double prec1=EM_CINEMATIC_K*time_diff;
    double prec2=0.5*EM_CINEMATIC_K*time_diff*time_diff;
    for(int i=0; i<EM_ACTION_N_VECTOR; i++)
    {
	incs[i]=action_vector_posf[i]*time_diff;
	if(action_vector_press[i][0] && !action_vector_press[i][1])
	{
	    action_vector_posf[i]+=prec1;
	    incs[i]+=prec2;
	}
	else if(action_vector_press[i][1] && !action_vector_press[i][0])
	{
	    action_vector_posf[i]-=prec1;
	    incs[i]-=prec2;
	}
	if(fabs(action_vector_posf[i])<EM_STOP_VECTOR_MOVE) action_vector_posf[i]=0; // finish abruptly
	else action_vector_posf[i]*=att;
    }    
}

void event_mng::action_vector_resetf(int i_vector)
{
    if(i_vector<0)
	for(int i=0; i<EM_ACTION_N_VECTOR; i++)
	    action_vector_posf[i]=0;
    else
	action_vector_posf[i_vector]=0;
}

void event_mng::action_vector_reset(int i_vector)
{
    if(i_vector<0)
	for(int i=0; i<EM_ACTION_N_VECTOR; i++)
	    action_vector_pos[i]=0;
    else
	action_vector_pos[i_vector]=0;
}

void event_mng::action_vector_keyboard_reset(int i_vector)
{
    if(i_vector<0)
	for(int i=0; i<EM_ACTION_N_VECTOR; i++)
	    action_vector_press[i][0]=action_vector_press[i][1]=false;
    else
	action_vector_press[i_vector][0]=action_vector_press[i_vector][1]=false;
}

bool event_mng::action_vector_active()
{
    for(int i=0; i<EM_ACTION_N_VECTOR; i++)
	if(action_vector_press[i][0] || action_vector_press[i][1]) return true;
    return false;
}

bool event_mng::action_vector_reset_if_opposite(int i)
{
    if(action_vector_press[i][0] && action_vector_press[i][1])
    {
	action_vector_press[i][0]=action_vector_press[i][1]=false;
	return true;
    }
    return false;
}

void event_mng::action_register_selector()
{
    struct
    {
	unsigned act_type, act_seq, act_coord, act_cursor, act_key;
	unsigned key_code, key_status;
	unsigned obj_id, obj_status;
	unsigned selector;
	const char *tip;
    } act[]=
      {
	  {
	      EM_ACTION_TYPE_CENTER, EM_ACTION_SEQ_ONCE, EM_ACTION_COORD_NONE, EM_ACTION_CURSOR_NORMAL, EM_ACTION_KEY_NORMAL,
	      EM_BUTKEY_OFF | IS_BUTTON_LEFT, 0,
	      IS_TYPE_ANY, IS_STATUS_ANY, IS_SEL_CENTER,
	      "Obj: Center"
	  },
	  {
	      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL
	  }
      };

    for(int i=0; act[i].tip; i++)
	action_register(
	    EM_SET_ACTION(act[i].act_type, act[i].act_seq, act[i].act_coord, act[i].act_cursor, act[i].act_key),
	    act[i].key_code, act[i].key_status, act[i].obj_id, act[i].obj_status, act[i].selector, act[i].tip);
}
